package edu.kit.ksri.db.mturk.utils.messaging;

import java.io.File;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.log4j.Logger;

import com.amazonaws.mturk.requester.Assignment;

import edu.kit.ksri.db.mturk.plugins.correctness.answers.AbstractAssignmentAnswer;
import edu.kit.ksri.db.mturk.var.Icd;
import edu.kit.ksri.db.mturk.var.StaticInformation;
import edu.kit.ksri.db.mturk.workerpools.ICDWorker;
import edu.kit.ksri.db.mturk.workerpools.PoolManager;
import edu.kit.ksri.db.mturk.workerpools.SimWorker;
import edu.kit.ksri.db.mturk.workerpools.StandardWorker;

/**
 * Copyright (c) 2010-2011 Karlsruhe Institute of Technology (KIT), Germany
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 * 
 * --------------------------------------------------------------------------
 * 
 * Software: CSP/WMV tool for dynamic quality management of microtasks
 * http://www.ksri.kit.edu/Upload/Users/PeopleClouds/HCOMP2011_An_extendable_toolkit_for_managing_quality_of_human-based_eServices.pdf
 * 
 * Organization: Karlsruhe Institute of Technology (KIT), Germany
 * http://www.kit.edu
 * 
 * Project: PeopleClouds project of the Karlsruhe Service Research Institute(KSRI)
 * http://www.ksri.kit.edu/Default.aspx?PageId=818
 * 
 * Contributing authors: David Bermbach, Sandra Rath, Pascal Wichmann, Robert Kern
 */
public class Result implements Comparable<Result>{

	private static final Logger basiclog = Logger.getLogger("basic." + Result.class);
	
	/** the hit id of the assignments */
	private final String hitId;

	/** a collection of assignments */
	private final List<Assignment> assignments = new ArrayList<Assignment>();

	/**
	 * reflects whether the weighted majority vote algorithm has decided that
	 * the probability of selecting the correct result is high enough
	 */
	private boolean isFinalResult = false;

	/**
	 * when isFinalResult is true, this object should contain the correct
	 * solution as determined by the WMV
	 */
	private AbstractAssignmentAnswer correctSolution = null;

	/**
	 * when isFinalResult is true, this object should contain the correct
	 * solution's value for phi_y (as determined by the WMV)
	 */
	private double phiCorrect = 0;
	
	
	/**
	 * this variable should contain the current value of the highest phi_y
	 */
	private double currentPhi = 0;

	/** is set as soon as correctSolution is set */
	private Date completionTime = null;
	
	/** save ratings of the workers for analysis */
	private final ConcurrentHashMap<String, Double> workerRatingsAtSubmitTime = new ConcurrentHashMap<String, Double>();

	/**
	 * @param hitId
	 * @param assignments
	 */
	public Result(String hitId, Assignment[] assignments) {
		super();
		this.hitId = hitId;
		for (Assignment e : assignments)
			this.assignments.add(e);
	}

	/**
	 * @param hitId
	 * @param assignments
	 */
	public Result(String hitId, List<Assignment> assignments) {
		super();
		this.hitId = hitId;
		this.assignments.addAll(assignments);
	}

	/**
	 * @param hitId
	 * @param assignment
	 */
	public Result(String hitId, Assignment assignment) {
		super();
		this.hitId = hitId;
		this.assignments.add(assignment);
	}

	/**
	 * merges two {@link Result} objects
	 * 
	 * @param one
	 * @param two
	 * @return a merged version of the results
	 * @throws RuntimeException
	 *             if the hit ids of the two results differ.
	 */
	public static Result mergeResults(Result one, Result two) {
		if (!(one.hitId.equals(two.hitId)))
			throw new RuntimeException(
					"Cannot merge results with different HIT IDs:"
							+ "\n\tID 1:" + one.hitId + "\n\tID 2:" + two.hitId);
		/*
		Result merged = new Result(one.getHitId(), one.getAssignments());
		merged.assignments.addAll(two.getAssignments());
		return merged;
		*/
		one.assignments.addAll(two.getAssignments());
		return one;
	}

	@Override
	public boolean equals(Object obj) {
		if (!(obj instanceof Result))
			return false;
		Result r = (Result) obj;
		return this.getHitId().equals(r.getHitId());

	}

	@Override
	public int hashCode() {
		return this.getHitId().hashCode();
	}

	/**
	 * @return the hitId
	 */
	public String getHitId() {
		synchronized (hitId) {
			return this.hitId;
		}
	}

	/**
	 * @return the assignments
	 */
	public List<Assignment> getAssignments() {
		synchronized (assignments) {
			return this.assignments;
		}
	}

	/**
	 * @return the isFinalResult
	 */
	public boolean isFinalResult() {
		return this.isFinalResult;
	}

	/**
	 * @param isFinalResult
	 *            the isFinalResult to set
	 */
	public void setFinalResult(boolean isFinalResult) {
		
		this.isFinalResult = isFinalResult;
		if (isFinalResult)
			ResultOutputCollector.addCompletedHit(hitId);
	}

	/**
	 * @return the correctSolution
	 */
	public AbstractAssignmentAnswer getCorrectSolution() {
		return this.correctSolution;
	}

	/**
	 * @param correctSolution
	 *            the correctSolution to set
	 */
	public void setCorrectSolution(AbstractAssignmentAnswer correctSolution) {
		this.correctSolution = correctSolution;
		completionTime = new Date();
	}

	/**
	 * @return the phiCorrect
	 */
	public double getPhiCorrect() {
		return this.phiCorrect;
	}

	/**
	 * @param phiCorrect
	 *            the phiCorrect to set
	 */
	public void setPhiCorrect(double phiCorrect) {
		this.phiCorrect = phiCorrect;
	}

	/**
	 * 
	 * @return a {@link String} value which can be persisted directly
	 */
	public String getPersistableString(int indexOfHit) {
		final String prefix = "\"", suffix = "\";";
		StringBuffer res = new StringBuffer("");
		res.append(prefix + hitId + suffix);
		res.append(prefix + (indexOfHit+1) + suffix);
		if ((int) Math.round(phiCorrect) == -1) {
			res.append(prefix + "NOT INSPECTED" + suffix);
		} else if ((int) Math.round(phiCorrect) == -2) {
			res.append(prefix + "ESCALATED" + suffix);
		} else
			res.append(prefix + ("" + phiCorrect).replace('.', ',') + suffix);
		res.append(prefix + completionTime + suffix);
		res.append(prefix + assignments.size() + suffix);
		if (isFinalResult)
			res.append(prefix + "correct." + hitId + ".xml" + suffix);
		else
			res.append(prefix + "N/A" + suffix);
		res.append(prefix + "all." + hitId + ".xml" + suffix);
		return res.toString();
	}

	/**
	 * 
	 * @return the column headings for getPersistableString()
	 */
	public static String getPersistableStringColumnHeadings() {
		final String prefix = "\"", suffix = "\";";
		String res = "";
		res += prefix + "HIT ID" + suffix;
		res += prefix + "Mapping number" + suffix;
		res += prefix + "Phi" + suffix;
		res += prefix + "Finalization Time" + suffix;
		res += prefix + "Number of Assignments" + suffix;
		res += prefix + "Filename of correct solution" + suffix;
		res += prefix + "Filename of all solutions" + suffix;

		return res;
	}
	
	/**
	 * 
	 * @return the column headings for getPersistableStringForAnaylsis()
	 */
	public static String getPersistableStringColumnHeadingsForAnalysis(){
		final String prefix = "\"", suffix = "\";";
		String res = "";

		res += prefix + "HIT ID" + suffix;
		res += prefix + "Mapping number" + suffix;
		res += prefix + "Scenario" + suffix;
		res += prefix + "HIT Type ID" + suffix;
		res += prefix + "Reward" + suffix;
		res += prefix + "Number of Assignments" + suffix;
		res += prefix + "Assignment ID" + suffix;
		res += prefix + "Assignment Submit Time" + suffix;
		res += prefix + "Worker ID" + suffix;
		res += prefix + "Finalization Time" + suffix;
		res += prefix + "Status" + suffix;
		
		if(StaticInformation.scenario instanceof Icd){
			
			for(int i = 1; i <= Icd.maxNumberOfLabels; i++){
				res += prefix + "Answer " + i + suffix;
			}
			
			res += prefix + "Comment" + suffix;
			res += prefix + "Total Assignments" + suffix;
			res += prefix + "Correctness" + suffix;
			res += prefix + "Sensitivity" + suffix;
			res += prefix + "Specificity" + suffix;
		}
		else{
			res += prefix + "Answer" + suffix;
			res += prefix + "PhiCorrect" + suffix;
			res += prefix + "Correctness" + suffix;
			res += prefix + "Total Assignments" + suffix;
			res += prefix + "Correct" + suffix;
			res += prefix + "False" + suffix;
		}
		
		return res;
	}
	
	/**
	 * 
	 * @return a {@link String} value which can be persisted directly
	 */
	public String getPersistableStringForAnalysis(String mode, int indexOfHit) {
		//Poker or OCR, we want correct, but have not a correct
		if(mode.equals("correct") && this.getCorrectSolution() == null && !(StaticInformation.scenario instanceof Icd))
			return "";

		final String prefix = "\"", suffix = "\";";
		StringBuffer res = new StringBuffer("");
		boolean newLine = false;
		boolean correctFoundAlready = false;
		
		for(Assignment assignment : assignments){
			//ICD, we want the worker answers, but we have the generated correct
			if((StaticInformation.scenario instanceof Icd) && (assignment.getWorkerId() == "ICDQuality") && mode.equals("analysis"))
				continue;
			
			//ICD, we want correct, but we have not a correct
			if((StaticInformation.scenario instanceof Icd) && (assignment.getWorkerId() != "ICDQuality") && mode.equals("correct"))
				continue;
			
			//OCR or Poker, we already found a correct, so skip all following assignments in correct mode
			if(mode.equals("correct") && correctFoundAlready)
				continue;

			if(mode.equals("correct")){
				//The answer of the assignment is not the same as the correct answer, so skip
				if(!assignment.getAnswer().equals(correctSolution.getRawAnswer()))
					continue;
				else
					correctFoundAlready = true;
			}
					
			if(mode.equals("analysis")){
				//newLine = true;
			}

			res.append(prefix + hitId + suffix);
			res.append(prefix + (indexOfHit+1) + suffix);
			res.append(prefix + StaticInformation.scenario.getClass().getSimpleName() + suffix);
			res.append(prefix + StaticInformation.scenario.getHitTypeID() + suffix);
			res.append(prefix + StaticInformation.scenario.getHitReward() + suffix);
			res.append(prefix + assignments.size() + suffix);
			res.append(prefix + assignment.getAssignmentId() + suffix);
			res.append(prefix + assignment.getSubmitTime().getTime() + suffix);
			res.append(prefix + assignment.getWorkerId() + suffix);
			res.append(prefix + completionTime + suffix);
			String status;
			
			if(isFinalResult){
				status = "Final";
			}
			else if(phiCorrect == -1){
				status = "Not inspected";
			}
			else if (phiCorrect == -2){
				status = "Escalated";
			}
			else if(!isFinalResult){
				status = "Incompleted";
			}
			else{
				status = "Unknown Error";
			}
			
			res.append(prefix + status + suffix);
			
			if(StaticInformation.scenario instanceof Icd){
				HashMap<String, String> answers = AbstractAssignmentAnswer.parseAnswer(assignment.getAnswer());

				String comment = answers.get("comment");
				int answersCount;
				
				if(comment == null){
					answersCount = (answers.size());
				}
				else{
					answersCount = (answers.size()-1);
					comment = comment.replace("\r", " ");
				}
				
				int labelCount = answersCount / (Icd.flagCount + 1);

				for(int j = 1; j <= labelCount; j++){
					
					String code = answers.get("code"+String.valueOf(j));
					String flag1 = answers.get("code"+String.valueOf(j)+"flag1");
					String flag2 = answers.get("code"+String.valueOf(j)+"flag2");
					String flag3 = answers.get("code"+String.valueOf(j)+"flag3");
					String flag4 = answers.get("code"+String.valueOf(j)+"flag4");
					
					//change false|true answers to true
					if (flag1.contains("true")) {
						flag1 = "true";				
					}
					if (flag2.contains("true")) {
						flag2 = "true";				
					}
					if (flag3.contains("true")) {
						flag3 = "true";				
					}
					if (flag4.contains("true")) {
						flag4 = "true";				
					}
					
					String label = code+" "+flag1+" "+flag2+" "+flag3+" "+flag4;
					label = label.trim();
					if(label.equals("false false false false")){
						res.append(prefix + "" + suffix);
					}
					else{
						res.append(prefix + label + suffix);
					}
				}
				
				if(labelCount <= Icd.maxNumberOfLabels){
					for(int k = 0; k < (Icd.maxNumberOfLabels - labelCount); k++){
						String label = "";
						res.append(prefix + label + suffix);
					}
				}
				
				if(comment == null)
					res.append(prefix + "" + suffix);
				else
					res.append(prefix + comment + suffix);
				
				ICDWorker icdWorker = (ICDWorker) PoolManager.getInstance().getPoolForName("ACTIVE").getWorkerForID(assignment.getWorkerId());
				if(assignment.getWorkerId() == "ICDQuality"){
					res.append(prefix + "" + suffix);
					res.append(prefix + "" + suffix);
					res.append(prefix + "" + suffix);
					res.append(prefix + "" + suffix);
				}
				else{
					res.append(prefix + icdWorker.getTotalAssignments() + suffix);
					res.append(prefix + icdWorker.getCorrectnessLevel() + suffix);
					res.append(prefix + icdWorker.getAverageSensitivity() + suffix);
					res.append(prefix + icdWorker.getAverageSpecificity() + suffix);
				}
			}
			else if(StaticInformation.scenario.getPrefix().equals("AddressResearch")){
				SimWorker w = (SimWorker) PoolManager.getInstance().getPoolForName("ACTIVE").getWorkerForID(assignment.getWorkerId());
				res.append(prefix + AbstractAssignmentAnswer.parseAnswer(assignment.getAnswer()) + suffix);
				res.append(prefix + phiCorrect + suffix);
				res.append(prefix + workerRatingsAtSubmitTime.get(w.getWorkerId()) + suffix);
				res.append(prefix + w.getCorrectnessLevel() + suffix);
				//res.append(prefix + w.getTotalInspectedCorrect() + suffix);
				//res.append(prefix + w.getTotalInspectedFalse() + suffix);
			}
			
			else{
				StandardWorker w = (StandardWorker) PoolManager.getInstance().getPoolForName("ACTIVE").getWorkerForID(assignment.getWorkerId());
				res.append(prefix + AbstractAssignmentAnswer.parseAnswer(assignment.getAnswer()) + suffix);
				res.append(prefix + phiCorrect + suffix);
				res.append(prefix + w.getCorrectnessLevel() + suffix);
				res.append(prefix + w.getTotalAssignments() + suffix);
				res.append(prefix + w.getTotalInspectedCorrect() + suffix);
				res.append(prefix + w.getTotalInspectedFalse() + suffix);
			}
			
			res.append("\n");
		}
		if(newLine){
			res.append("\n");
		}
		
		return res.toString();
	}

	/**
	 * exports all assignments to the specified folder with filenames<br>
	 * 1) <code>correct.[HIT ID].xml</code> and <br>
	 * 2) <code>all.[HIT ID].xml</code>
	 * 
	 * @param folder
	 * @return true when the export was successful
	 */
	public boolean persist(String folder, int mappingNumber) {

		try {
			PrintWriter pw = null;
			// export correct result if it exists
			if (correctSolution != null) {
				File correct = new File(folder + "/correct." + hitId + ".xml");
				pw = new PrintWriter(correct);
				String solution = correctSolution.getRawAnswer();
				if (solution.startsWith("<?"))
					solution = solution.substring(solution.indexOf(">") + 1);
				pw.println("<?xml version=\"1.0\"?><CorrectResult hitid=\""
						+ hitId + "\" phi=\"" + phiCorrect + "\">" + solution
						+ "</CorrectResult>");
				pw.flush();
				pw.close();
			}
			
			if(!(StaticInformation.scenario instanceof Icd) || ((StaticInformation.scenario instanceof Icd) && !isFinalResult)){
				File all = new File(folder + "/all." + hitId + ".xml");
				pw = new PrintWriter(all);
				pw.println("<?xml version=\"1.0\"?><AllResults hitid=\"" + hitId
						+ "\" mappingNumber=\"" + (mappingNumber+1) + "\" numberOfResults=\"" + assignments.size() + "\">");
				for (Assignment a : assignments) {
					String solution = a.getAnswer();
					if (solution.startsWith("<?"))
						solution = solution.substring(solution.indexOf(">") + 1);
					pw.println("<Result worker=\"" + a.getWorkerId()
							+ "\" assignmentID=\""
							+ a.getAssignmentId()
							+ "\" submissionTime=\""
							+ new Date(a.getSubmitTime().getTimeInMillis()) + "\">"
							+ solution + "</Result>");
				}
				pw.print("</AllResults>");
				pw.flush();
				pw.close();
			}
		} catch (Exception e) {
			basiclog.error("Error while exporting hit id " + hitId + ": " + e);
			return false;
		}
		return true;
	}

	/**
	 * @return the completionTime
	 */
	public synchronized Date getCompletionTime() {
		return this.completionTime;
	}
	
	public double getCurrentPhi(){
		return currentPhi;
	}
	
	public void setCurrentPhi(double phi){
		this.currentPhi = phi;
	}

	public int compareTo(Result arg0) {
		if (this.getCurrentPhi() > arg0.getCurrentPhi())
			return -1;
		if (this.getCurrentPhi() < arg0.getCurrentPhi())
			return 1;
		return 0;
	}
	
	public void setWorkerRating(String workerID, double rating){
		workerRatingsAtSubmitTime.put(workerID, rating);
	}
}
